﻿Option Explicit On
Option Strict On

Imports System
Imports System.IO
Imports System.Xml
Imports System.Collections

Public Class XmlConfigSource
    Inherits ConfigSourceBase

#Region " Private Members "

    Private m_configDoc As XmlDocument = Nothing
    Private m_savePath As String = Nothing

#End Region

#Region " Constructors "

    ''' <summary>
    ''' 
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New()
        m_configDoc = New XmlDocument()
        m_configDoc.LoadXml("<YOM/>")
        PerformLoad(m_configDoc)
    End Sub

    ''' <summary>
    ''' 
    ''' </summary>
    ''' <param name="path"></param>
    ''' <remarks></remarks>
    Public Sub New(path As String)
        Load(path)
    End Sub

    ''' <summary>
    ''' 
    ''' </summary>
    ''' <param name="reader"></param>
    ''' <remarks></remarks>
    Public Sub New(reader As XmlReader)
        Load(reader)
    End Sub

#End Region

#Region " Public properties "

    Public ReadOnly Property SavePath() As String
        Get
            Return m_savePath
        End Get
    End Property

#End Region

#Region " Public Methods "

    Public Sub Load(path As String)
        m_savePath = path
        m_configDoc = New XmlDocument()
        m_configDoc.Load(path)
        PerformLoad(m_configDoc)
    End Sub

    Public Sub Load(reader As XmlReader)
        m_configDoc = New XmlDocument()
        m_configDoc.Load(reader)
        PerformLoad(m_configDoc)
    End Sub

    Public Overrides Sub Save()
        If Not IsSavable() Then
            Throw New ArgumentException("Source cannot be saved in this state")
        End If

        MergeConfigsIntoDocument()
        m_configDoc.Save(m_savePath)
        MyBase.Save()
    End Sub

    Public Overloads Sub Save(path As String)
        Me.m_savePath = path
        Me.Save()
    End Sub

    Public Overloads Sub Save(writer As TextWriter)
        MergeConfigsIntoDocument()
        m_configDoc.Save(writer)
        m_savePath = Nothing
        OnSaved(New EventArgs())
    End Sub

    Public Overloads Sub Save(stream As Stream)
        MergeConfigsIntoDocument()
        m_configDoc.Save(stream)
        m_savePath = Nothing
        OnSaved(New EventArgs())
    End Sub

    Public Overrides Sub Reload()
        If m_savePath Is Nothing Then
            Throw New ArgumentException("Error reloading: You must have " + "the loaded the source from a file")
        End If

        m_configDoc = New XmlDocument()
        m_configDoc.Load(m_savePath)
        MergeDocumentIntoConfigs()
        MyBase.Reload()
    End Sub

    Public Overrides Function ToString() As String
        MergeConfigsIntoDocument()
        Using writer As New StringWriter(Globalization.CultureInfo.InvariantCulture)
            m_configDoc.Save(writer)

            Return writer.ToString()
        End Using
    End Function

#End Region

#Region " Private methods "

    ''' <summary>
    ''' Merges all of the configs from the config collection into the 
    ''' XmlDocument.
    ''' </summary>
    Private Sub MergeConfigsIntoDocument()
        RemoveSections()
        For Each config As IConfig In Me.Configs
            Dim keys As String() = config.GetKeys()

            Dim node As XmlNode = GetSectionByName(config.Name)
            If node Is Nothing Then
                node = SectionNode(config.Name)
                m_configDoc.DocumentElement.AppendChild(node)
            End If
            RemoveKeys(config.Name)

            For i As Integer = 0 To keys.Length - 1
                SetKey(node, keys(i), config.GetValue(keys(i)))
            Next
        Next
    End Sub

    ''' <summary>
    ''' Removes all XML sections that were removed as configs.
    ''' </summary>
    Private Sub RemoveSections()
        Dim attr As XmlAttribute = Nothing

        For Each node As XmlNode In m_configDoc.DocumentElement.ChildNodes
            If node.NodeType = XmlNodeType.Element AndAlso node.Name = "Section" Then

                attr = node.Attributes("Name")
                If attr IsNot Nothing Then
                    If Me.Configs(attr.Value) Is Nothing Then
                        m_configDoc.DocumentElement.RemoveChild(node)
                    End If
                Else
                    Throw New ArgumentException("Section name attribute not found")
                End If
            End If
        Next
    End Sub

    ''' <summary>
    ''' Removes all XML keys that were removed as config keys.
    ''' </summary>
    Private Sub RemoveKeys(sectionName As String)
        Dim sectionNode As XmlNode = GetSectionByName(sectionName)
        Dim keyName As XmlAttribute = Nothing

        If sectionNode IsNot Nothing Then
            For Each node As XmlNode In sectionNode.ChildNodes
                If node.NodeType = XmlNodeType.Element AndAlso node.Name = "Key" Then

                    keyName = node.Attributes("Name")
                    If keyName IsNot Nothing Then
                        If Me.Configs(sectionName).GetValue(keyName.Value) Is Nothing Then
                            sectionNode.RemoveChild(node)
                        End If
                    Else
                        Throw New ArgumentException("Name attribute not found in key")
                    End If
                End If
            Next
        End If
    End Sub

    ''' <summary>
    ''' Loads all sections and keys.
    ''' </summary>
    Private Sub PerformLoad(document As XmlDocument)
        Me.Configs.Clear()

        Me.Merge(Me)
        ' required for SaveAll
        If document.DocumentElement.Name <> "YOM" Then
            Throw New ArgumentException("Did not find YOM XML root node")
        End If

        LoadSections(document.DocumentElement)
    End Sub

    ''' <summary>
    ''' Loads all configuration sections.
    ''' </summary>
    Private Sub LoadSections(rootNode As XmlNode)
        Dim config As ConfigBase = Nothing

        For Each child As XmlNode In rootNode.ChildNodes
            If child.NodeType = XmlNodeType.Element AndAlso child.Name = "Section" Then
                config = New ConfigBase(child.Attributes("Name").Value, Me)
                Me.Configs.Add(config)
                LoadKeys(child, config)
            End If
        Next
    End Sub

    ''' <summary>
    ''' Loads all keys for a config.
    ''' </summary>
    Private Shared Sub LoadKeys(node As XmlNode, config As ConfigBase)
        For Each child As XmlNode In node.ChildNodes
            If child.NodeType = XmlNodeType.Element AndAlso child.Name = "Key" Then
                config.Add(child.Attributes("Name").Value, child.Attributes("Value").Value)
            End If
        Next
    End Sub

    ''' <summary>
    ''' Sets an XML key.  If it does not exist then it is created.
    ''' </summary>
    Private Sub SetKey(sectionNode As XmlNode, key As String, value As String)
        Dim node As XmlNode = GetKeyByName(sectionNode, key)

        If node Is Nothing Then
            CreateKey(sectionNode, key, value)
        Else
            node.Attributes("Value").Value = value
        End If
    End Sub

    ''' <summary>
    ''' Creates a key node and adds it to the collection at the end.
    ''' </summary>
    Private Sub CreateKey(sectionNode As XmlNode, key As String, value As String)
        Dim node As XmlNode = m_configDoc.CreateElement("Key")
        Dim keyAttr As XmlAttribute = m_configDoc.CreateAttribute("Name")
        Dim valueAttr As XmlAttribute = m_configDoc.CreateAttribute("Value")
        keyAttr.Value = key
        valueAttr.Value = value

        node.Attributes.Append(keyAttr)
        node.Attributes.Append(valueAttr)

        sectionNode.AppendChild(node)
    End Sub

    ''' <summary>
    ''' Returns a new section node.
    ''' </summary>
    Private Function SectionNode(name As String) As XmlNode
        Dim result As XmlNode = m_configDoc.CreateElement("Section")
        Dim nameAttr As XmlAttribute = m_configDoc.CreateAttribute("Name")
        nameAttr.Value = name
        result.Attributes.Append(nameAttr)

        Return result
    End Function

    ''' <summary>
    ''' Returns a section node by name.
    ''' </summary>
    Private Function GetSectionByName(name As String) As XmlNode
        Dim result As XmlNode = Nothing

        For Each node As XmlNode In m_configDoc.DocumentElement.ChildNodes
            If node.NodeType = XmlNodeType.Element AndAlso node.Name = "Section" AndAlso node.Attributes("Name").Value = name Then
                result = node
                Exit For
            End If
        Next

        Return result
    End Function

    ''' <summary>
    ''' Returns a key node by name.
    ''' </summary>
    Private Shared Function GetKeyByName(sectionNode As XmlNode, name As String) As XmlNode
        Dim result As XmlNode = Nothing

        For Each node As XmlNode In sectionNode.ChildNodes
            If node.NodeType = XmlNodeType.Element AndAlso node.Name = "Key" AndAlso node.Attributes("Name").Value = name Then
                result = node
                Exit For
            End If
        Next

        Return result
    End Function

    ''' <summary>
    ''' Returns true if this instance is savable.
    ''' </summary>
    Private Function IsSavable() As Boolean
        Return (Me.m_savePath IsNot Nothing AndAlso m_configDoc IsNot Nothing)
    End Function

    ''' <summary>
    ''' Merges the XmlDocument into the Configs when the document is 
    ''' reloaded.  
    ''' </summary>
    Private Sub MergeDocumentIntoConfigs()
        ' Remove all missing configs first
        RemoveConfigs()

        For Each node As XmlNode In m_configDoc.DocumentElement.ChildNodes
            ' If node is a section node
            If node.NodeType = XmlNodeType.Element AndAlso node.Name = "Section" Then

                Dim sectionName As String = node.Attributes("Name").Value
                Dim config As IConfig = Me.Configs(sectionName)
                If config Is Nothing Then
                    ' The section is new so add it
                    config = New ConfigBase(sectionName, Me)
                    Me.Configs.Add(config)
                End If
                RemoveConfigKeys(config)
            End If
        Next
    End Sub

    ''' <summary>
    ''' Removes all configs that are not in the newly loaded XmlDocument.  
    ''' </summary>
    Private Sub RemoveConfigs()
        Dim config As IConfig = Nothing
        For i As Integer = Me.Configs.Count - 1 To -1 + 1 Step -1
            config = Me.Configs(i)
            ' If the section is not present in the XmlDocument
            If GetSectionByName(config.Name) Is Nothing Then
                Me.Configs.Remove(config)
            End If
        Next
    End Sub

    ''' <summary>
    ''' Removes all XML keys that were removed as config keys.
    ''' </summary>
    Private Sub RemoveConfigKeys(config As IConfig)
        Dim section As XmlNode = GetSectionByName(config.Name)

        ' Remove old keys
        Dim configKeys As String() = config.GetKeys()
        For Each configKey As String In configKeys
            If GetKeyByName(section, configKey) Is Nothing Then
                ' Key doesn't exist, remove
                config.Remove(configKey)
            End If
        Next

        ' Add or set all new keys
        For Each node As XmlNode In section.ChildNodes
            ' Loop through all key nodes and add to config
            If node.NodeType = XmlNodeType.Element AndAlso node.Name = "Key" Then
                config.SetValue(node.Attributes("Name").Value, node.Attributes("Value").Value)
            End If
        Next
    End Sub

#End Region

End Class
